Lær å håndtere samtidige Promises i JavaScript med Promise.all, allSettled, race og any. Bygg raskere, mer robuste applikasjoner med parallellprosessering.
Mestre JavaScript-samtidighet: Et dypdykk i parallell behandling av Promises
I moderne webutvikling er ytelse ikke en funksjon; det er et fundamentalt krav. Brukere over hele verden forventer at applikasjoner er raske, responsive og sømløse. Kjernen i denne ytelsesutfordringen, spesielt i JavaScript, er konseptet med å håndtere asynkrone operasjoner effektivt. Fra å hente data fra et API til å lese en fil eller spørre en database, er det mange oppgaver som ikke fullføres umiddelbart. Hvordan vi håndterer disse venteperiodene kan utgjøre forskjellen mellom en treg applikasjon og en herlig flytende brukeropplevelse.
JavaScript er i sin natur et entrådet språk. Dette betyr at det bare kan utføre én kodebit om gangen. Dette kan høres ut som en begrensning, men JavaScripts hendelsesløkke (event loop) og ikke-blokkerende I/O-modell lar den håndtere asynkrone oppgaver med utrolig effektivitet. Den moderne hjørnesteinen i denne modellen er Promise – et objekt som representerer den endelige fullføringen (eller feilen) av en asynkron operasjon.
Men å bare bruke Promises eller den elegante `async/await`-syntaksen garanterer ikke automatisk optimal ytelse. En vanlig fallgruve for utviklere er å håndtere flere uavhengige asynkrone oppgaver sekvensielt, noe som skaper unødvendige flaskehalser. Det er her samtidig behandling av promises (concurrent promise processing) kommer inn i bildet. Ved å starte flere asynkrone operasjoner parallelt og vente på dem samlet, kan vi dramatisk redusere total kjøretid og bygge langt mer effektive applikasjoner.
Denne omfattende guiden vil ta deg med på et dypdykk inn i verden av JavaScript-samtidighet. Vi vil utforske verktøyene som er innebygd direkte i språket – `Promise.all()`, `Promise.allSettled()`, `Promise.race()` og `Promise.any()` – for å hjelpe deg med å orkestrere parallelle oppgaver som en proff. Enten du er en juniorutvikler som prøver å forstå asynkronitet eller en erfaren ingeniør som ønsker å finpusse mønstrene dine, vil denne artikkelen utstyre deg med kunnskapen til å skrive raskere, mer robust og mer sofistikert JavaScript-kode.
Først, en rask avklaring: Samtidighet vs. Parallellisme
Før vi fortsetter, er det viktig å avklare to begreper som ofte brukes om hverandre, men som har distinkte betydninger i informatikk: samtidighet og parallellisme.
- Samtidighet er konseptet med å håndtere flere oppgaver over en tidsperiode. Det handler om å håndtere mange ting på en gang. Et system er samtidig (concurrent) hvis det kan starte, kjøre og fullføre mer enn én oppgave uten å vente på at den forrige skal bli ferdig. I JavaScripts entrådede miljø oppnås samtidighet via hendelsesløkken, som lar motoren bytte mellom oppgaver. Mens en langvarig oppgave (som en nettverksforespørsel) venter, kan motoren jobbe med andre ting.
- Parallellisme er konseptet med å utføre flere oppgaver samtidig. Det handler om å gjøre mange ting på en gang. Ekte parallellisme krever en flerkjerneprosessor, der forskjellige tråder kan kjøre på forskjellige kjerner på nøyaktig samme tid. Mens web workers tillater ekte parallellisme i nettleserbasert JavaScript, gjelder den kjerne-samtidighetsmodellen vi diskuterer her for den ene hovedtråden.
For I/O-bundne operasjoner (som nettverksforespørsler) gir JavaScripts samtidighetsmodell *effekten* av parallellisme. Vi kan initiere flere forespørsler samtidig. Mens JavaScript-motoren venter på svarene, er den fri til å gjøre annet arbeid. Operasjonene skjer 'parallelt' fra perspektivet til de eksterne ressursene (servere, filsystemer). Dette er den kraftige modellen vi skal utnytte.
Den sekvensielle fellen: Et vanlig anti-mønster
La oss starte med å identifisere en vanlig feil. Når utviklere først lærer `async/await`, er syntaksen så ren at det er lett å skrive kode som ser synkron ut, men som utilsiktet er sekvensiell og ineffektiv. Tenk deg at du må hente en brukers profil, deres siste innlegg og varslene deres for å bygge et dashbord.
En naiv tilnærming kan se slik ut:
Eksempel: Den ineffektive sekvensielle hentingen
async function fetchDashboardDataSequentially(userId) {
console.time('sequentialFetch');
console.log('Fetching user profile...');
const userProfile = await fetchUserProfile(userId); // Venter her
console.log('Fetching user posts...');
const userPosts = await fetchUserPosts(userId); // Venter her
console.log('Fetching user notifications...');
const userNotifications = await fetchUserNotifications(userId); // Venter her
console.timeEnd('sequentialFetch');
return { userProfile, userPosts, userNotifications };
}
// Tenk deg at disse funksjonene tar tid å fullføre
// fetchUserProfile -> 500ms
// fetchUserPosts -> 800ms
// fetchUserNotifications -> 1000ms
Hva er galt med dette bildet? Hvert `await`-nøkkelord pauser utførelsen av `fetchDashboardDataSequentially`-funksjonen til promiset er fullført. Forespørselen om `userPosts` starter ikke engang før `userProfile`-forespørselen er helt ferdig. Forespørselen om `userNotifications` starter ikke før `userPosts` er tilbake. Disse tre nettverksforespørslene er uavhengige av hverandre; det er ingen grunn til å vente! Den totale tiden vil være summen av alle de individuelle tidene:
Total tid ≈ 500ms + 800ms + 1000ms = 2300ms
Dette er en enorm ytelsesflaskehals. Vi kan gjøre det mye, mye bedre.
Frigjøre ytelse: Kraften i samtidig utførelse
Løsningen er å starte alle de asynkrone operasjonene samtidig, uten å `await`e dem umiddelbart. Dette lar dem kjøre samtidig. Vi kan lagre de ventende Promise-objektene i variabler og deretter bruke en Promise-kombinator for å vente på at alle skal fullføres.
Eksempel: Den effektive samtidige hentingen
async function fetchDashboardDataConcurrently(userId) {
console.time('concurrentFetch');
console.log('Initiating all fetches at once...');
const profilePromise = fetchUserProfile(userId);
const postsPromise = fetchUserPosts(userId);
const notificationsPromise = fetchUserNotifications(userId);
// Nå venter vi på at alle skal fullføres
const [userProfile, userPosts, userNotifications] = await Promise.all([
profilePromise,
postsPromise,
notificationsPromise
]);
console.timeEnd('concurrentFetch');
return { userProfile, userPosts, userNotifications };
}
I denne versjonen kaller vi de tre hentefunksjonene uten `await`. Dette starter umiddelbart alle tre nettverksforespørslene. JavaScript-motoren overlater dem til det underliggende miljøet (nettleseren eller Node.js) og mottar tre ventende Promises tilbake. Deretter brukes `Promise.all()` til å vente på at alle tre av disse promisene skal fullføres. Den totale tiden bestemmes nå av den lengstvarende operasjonen, ikke summen.
Total tid ≈ max(500ms, 800ms, 1000ms) = 1000ms
Vi har nettopp kuttet datainnhentingstiden vår med mer enn halvparten! Dette er det grunnleggende prinsippet for parallell behandling av promises. La oss nå utforske de kraftige verktøyene JavaScript tilbyr for å orkestrere disse samtidige oppgavene.
Verktøykassen for Promise-kombinatorer: `all`, `allSettled`, `race` og `any`
JavaScript tilbyr fire statiske metoder på `Promise`-objektet, kjent som promise-kombinatorer. Hver av dem tar en itererbar (som en matrise) av promises og returnerer ett enkelt, nytt promise. Oppførselen til dette nye promiset avhenger av hvilken kombinator du bruker.
1. `Promise.all()`: Alt-eller-ingenting-tilnærmingen
`Promise.all()` er det perfekte verktøyet når du har en gruppe oppgaver som alle er kritiske for neste steg. Det representerer den logiske "OG"-betingelsen: Oppgave 1 OG Oppgave 2 OG Oppgave 3 må alle lykkes.
- Input: En itererbar av promises.
- Oppførsel: Den returnerer ett enkelt promise som oppfylles (fulfills) når alle input-promisene er oppfylt. Den oppfylte verdien er en matrise (array) med resultatene fra input-promisene, i samme rekkefølge.
- Feilmodus: Den avviser (rejects) umiddelbart så snart ett av input-promisene avvises. Avvisningsårsaken er årsaken fra det første promiset som ble avvist. Dette kalles ofte "fail-fast"-oppførsel.
Brukstilfelle: Aggregering av kritiske data
Vårt dashbord-eksempel er et perfekt bruksområde. Hvis du ikke kan laste brukerens profil, gir det kanskje ikke mening å vise innleggene og varslene deres. Hele komponenten avhenger av at alle tre datapunktene er tilgjengelige.
// Hjelpefunksjon for å simulere API-kall
const mockApiCall = (value, delay, shouldFail = false) => {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (shouldFail) {
reject(new Error(`API call failed for: ${value}`));
} else {
console.log(`Resolved: ${value}`);
resolve({ data: value });
}
}, delay);
});
};
async function loadCriticalData() {
console.log('Using Promise.all for critical data...');
try {
const [profile, settings, permissions] = await Promise.all([
mockApiCall('userProfile', 400),
mockApiCall('userSettings', 700),
mockApiCall('userPermissions', 500)
]);
console.log('All critical data loaded successfully!');
// Nå kan UI rendres med profil, innstillinger og tillatelser
} catch (error) {
console.error('Failed to load critical data:', error.message);
// Vis en feilmelding til brukeren
}
}
// Hva skjer hvis en feiler?
async function loadCriticalDataWithFailure() {
console.log('\nDemonstrating Promise.all failure...');
try {
const results = await Promise.all([
mockApiCall('userProfile', 400),
mockApiCall('userSettings', 700, true), // Denne vil feile
mockApiCall('userPermissions', 500)
]);
} catch (error) {
console.error('Promise.all rejected:', error.message);
// Merk: 'userProfile'- og 'userPermissions'-kallene kan ha blitt fullført,
// men resultatene deres er tapt fordi hele operasjonen feilet.
}
}
loadCriticalData();
// Etter en forsinkelse, kall feileksempelet
setTimeout(loadCriticalDataWithFailure, 2000);
Fallgruve med `Promise.all()`
Den primære fallgruven er dens "fail-fast"-natur. Hvis du henter data til ti forskjellige, uavhengige widgeter på en side, og ett API feiler, vil `Promise.all()` avvise, og du vil miste resultatene for de ni andre vellykkede kallene. Det er her vår neste kombinator skinner.
2. `Promise.allSettled()`: Den robuste innsamleren
Introdusert i ES2020, var `Promise.allSettled()` en revolusjon for robusthet. Den er designet for når du vil vite utfallet av hvert enkelt promise, enten det lyktes eller feilet. Det avviser aldri.
- Input: En itererbar av promises.
- Oppførsel: Den returnerer ett enkelt promise som alltid oppfylles. Det oppfylles når alle input-promisene er avgjort (settled) (enten oppfylt eller avvist). Den oppfylte verdien er en matrise av objekter, der hvert objekt beskriver utfallet av et promise.
- Resultatformat: Hvert resultatobjekt har en `status`-egenskap.
- Hvis oppfylt: `{ status: 'fulfilled', value: theResult }`
- Hvis avvist: `{ status: 'rejected', reason: theError }`
Brukstilfelle: Ikke-kritiske, uavhengige operasjoner
Tenk deg en side som viser flere uavhengige komponenter: en vær-widget, en nyhetsstrøm og en aksjekurs-ticker. Hvis nyhetsstrømmens API feiler, vil du fortsatt vise været og aksjeinformasjonen. `Promise.allSettled()` er perfekt for dette.
async function loadDashboardWidgets() {
console.log('\nUsing Promise.allSettled for independent widgets...');
const results = await Promise.allSettled([
mockApiCall('Weather Data', 600),
mockApiCall('News Feed', 1200, true), // Dette API-et er nede
mockApiCall('Stock Ticker', 800)
]);
console.log('All promises have settled. Processing results...');
results.forEach((result, index) => {
if (result.status === 'fulfilled') {
console.log(`Widget ${index} loaded successfully with data:`, result.value.data);
// Render denne widgeten i brukergrensesnittet
} else {
console.error(`Widget ${index} failed to load:`, result.reason.message);
// Vis en spesifikk feiltilstand for denne widgeten
}
});
}
loadDashboardWidgets();
Med `Promise.allSettled()` blir applikasjonen din mye mer robust. Ett enkelt feilpunkt forårsaker ikke en kaskade som tar ned hele brukergrensesnittet. Du kan håndtere hvert utfall på en elegant måte.
3. `Promise.race()`: Førstemann til målstreken
`Promise.race()` gjør nøyaktig det navnet antyder. Det setter en gruppe promises opp mot hverandre og kårer en vinner så snart den første krysser målstreken, uavhengig av om det var en suksess eller en fiasko.
- Input: En itererbar av promises.
- Oppførsel: Den returnerer ett enkelt promise som avgjøres (oppfylles eller avvises) så snart det første av input-promisene avgjøres. Oppfyllelsesverdien eller avvisningsårsaken til det returnerte promiset vil være den samme som for det "vinnende" promiset.
- Viktig merknad: De andre promisene blir ikke kansellert. De vil fortsette å kjøre i bakgrunnen, og resultatene deres vil simpelthen bli ignorert av `Promise.race()`-konteksten.
Brukstilfelle: Implementere en tidsavbrudd (timeout)
Det vanligste og mest praktiske bruksområdet for `Promise.race()` er å håndheve en tidsavbrudd på en asynkron operasjon. Du kan "kappløpe" hovedoperasjonen din mot et `setTimeout`-promise. Hvis operasjonen din tar for lang tid, vil tidsavbrudd-promiset avgjøres først, og du kan håndtere det som en feil.
function createTimeout(delay) {
return new Promise((_, reject) => {
setTimeout(() => {
reject(new Error(`Operation timed out after ${delay}ms`));
}, delay);
});
}
async function fetchDataWithTimeout() {
console.log('\nUsing Promise.race for a timeout...');
try {
const result = await Promise.race([
mockApiCall('some critical data', 2000), // Dette vil ta for lang tid
createTimeout(1500) // Dette vil vinne kappløpet
]);
console.log('Data fetched successfully:', result.data);
} catch (error) {
console.error(error.message);
}
}
fetchDataWithTimeout();
Et annet bruksområde: Redundante endepunkter
Du kan også bruke `Promise.race()` til å spørre flere redundante servere for den samme ressursen og ta imot responsen fra den serveren som er raskest. Dette er imidlertid risikabelt, for hvis den raskeste serveren returnerer en feil (f.eks. en 500-statuskode), vil `Promise.race()` avvise umiddelbart, selv om en litt tregere server ville ha returnert et vellykket svar. Dette leder oss til vår siste, mer passende kombinator for dette scenarioet.
4. `Promise.any()`: Den første som lykkes
Introdusert i ES2021, er `Promise.any()` som en mer optimistisk versjon av `Promise.race()`. Den venter også på at det første promiset skal avgjøres, men den ser spesifikt etter det første som oppfylles.
- Input: En itererbar av promises.
- Oppførsel: Den returnerer ett enkelt promise som oppfylles så snart ett av input-promisene oppfylles. Oppfyllelsesverdien er verdien til det første promiset som ble oppfylt.
- Feilmodus: Den avviser bare hvis alle input-promisene avvises. Avvisningsårsaken er et spesielt `AggregateError`-objekt, som inneholder en `errors`-egenskap – en matrise med alle de individuelle avvisningsårsakene.
Brukstilfelle: Henting fra redundante kilder
Dette er det perfekte verktøyet for å hente en ressurs fra flere kilder, som primære og backup-servere eller flere Content Delivery Networks (CDN-er). Du bryr deg bare om å få ett vellykket svar så raskt som mulig.
async function fetchResourceFromMirrors() {
console.log('\nUsing Promise.any to find the fastest successful source...');
try {
const resource = await Promise.any([
mockApiCall('Primary CDN', 800, true), // Feiler raskt
mockApiCall('European Mirror', 1200), // Tregere, men vil lykkes
mockApiCall('Asian Mirror', 1100) // Lykkes også, men er tregere enn den europeiske
]);
console.log('Resource fetched successfully from a mirror:', resource.data);
} catch (error) {
if (error instanceof AggregateError) {
console.error('All mirrors failed to provide the resource.');
// Du kan inspisere individuelle feil:
error.errors.forEach(err => console.log('- ' + err.message));
}
}
}
fetchResourceFromMirrors();
I dette eksempelet vil `Promise.any()` ignorere den raske feilen fra den primære CDN-en og vente på at den europeiske speilserveren (mirror) skal oppfylles. Da vil den resolvere med den dataen og effektivt ignorere resultatet fra den asiatiske speilserveren.
Velge riktig verktøy for jobben: En rask guide
Med fire kraftige alternativer, hvordan bestemmer du hvilket du skal bruke? Her er et enkelt rammeverk for beslutningstaking:
- Trenger jeg resultatene fra ALLE promises, og er det en katastrofe hvis NOEN av dem feiler?
BrukPromise.all(). Dette er for tett koblede alt-eller-ingenting-scenarioer. - Trenger jeg å vite utfallet av ALLE promises, uavhengig av om de lykkes eller feiler?
BrukPromise.allSettled(). Dette er for å håndtere flere uavhengige oppgaver der du vil behandle hvert utfall og opprettholde applikasjonens robusthet. - Bryr jeg meg bare om det aller første promiset som blir ferdig, enten det er en suksess eller en fiasko?
BrukPromise.race(). Dette er primært for å implementere tidsavbrudd eller andre kappløpsbetingelser der det første resultatet (uansett type) er det eneste som betyr noe. - Bryr jeg meg bare om det første promiset som LYKKES, og kan jeg ignorere de som feiler?
BrukPromise.any(). Dette er for scenarioer som involverer redundans, som å prøve flere endepunkter for den samme ressursen.
Avanserte mønstre og hensyn fra den virkelige verden
Selv om promise-kombinatorene er utrolig kraftige, krever profesjonell utvikling ofte litt mer nyanse.
Begrensning av samtidighet og struping (throttling)
Hva skjer hvis du har en matrise med 1000 ID-er og vil hente data for hver av dem? Hvis du naivt sender alle 1000 promise-genererende kall inn i `Promise.all()`, vil du umiddelbart fyre av 1000 nettverksforespørsler. Dette kan ha flere negative konsekvenser:
- Serveroverbelastning: Du kan overvelde serveren du spør fra, noe som fører til feil eller redusert ytelse for alle brukere.
- Rategrenser: De fleste offentlige API-er har rategrenser. Du vil sannsynligvis nå grensen din og motta `429 Too Many Requests`-feil.
- Klientressurser: Klienten (nettleser eller server) kan slite med å håndtere så mange åpne nettverkstilkoblinger samtidig.
Løsningen er å begrense samtidigheten ved å behandle promisene i puljer (batches). Selv om du kan skrive din egen logikk for dette, håndterer modne biblioteker som `p-limit` eller `async-pool` dette elegant. Her er et konseptuelt eksempel på hvordan du kan nærme deg det manuelt:
async function processInBatches(items, batchSize, processingFn) {
let results = [];
for (let i = 0; i < items.length; i += batchSize) {
const batch = items.slice(i, i + batchSize);
console.log(`Processing batch starting at index ${i}...`);
const batchPromises = batch.map(processingFn);
const batchResults = await Promise.allSettled(batchPromises);
results = results.concat(batchResults);
}
return results;
}
// Eksempel på bruk:
const userIds = Array.from({ length: 20 }, (_, i) => i + 1);
// Vi vil behandle 20 brukere i puljer på 5
processInBatches(userIds, 5, id => mockApiCall(`user_${id}`, Math.random() * 1000))
.then(allResults => {
console.log('\nBatch processing complete.');
const successful = allResults.filter(r => r.status === 'fulfilled').length;
const failed = allResults.filter(r => r.status === 'rejected').length;
console.log(`Total Results: ${allResults.length}, Successful: ${successful}, Failed: ${failed}`);
});
En merknad om kansellering
En langvarig utfordring med native Promises er at de ikke kan kanselleres. Når du først har opprettet et promise, vil det kjøre til det er fullført. Mens `Promise.race` kan hjelpe deg med å ignorere et tregt resultat, fortsetter den underliggende operasjonen å konsumere ressurser. For nettverksforespørsler er den moderne løsningen `AbortController`-API-et, som lar deg signalisere til en `fetch`-forespørsel at den skal avbrytes. Integrering av `AbortController` med promise-kombinatorer kan gi en robust måte å håndtere og rydde opp i langvarige samtidige oppgaver.
Konklusjon: Fra sekvensiell til samtidig tenkning
Å mestre asynkron JavaScript er en reise. Den begynner med å forstå den entrådede hendelsesløkken, fortsetter med å bruke Promises og `async/await` for klarhet, og kulminerer i å tenke samtidig for å maksimere ytelsen. Å skifte fra en sekvensiell `await`-tankegang til en parallell-først-tilnærming er en av de mest effektfulle endringene en utvikler kan gjøre for å forbedre applikasjonens responsivitet.
Ved å utnytte de innebygde promise-kombinatorene, er du utstyrt for å håndtere et bredt spekter av reelle scenarioer med eleganse og presisjon:
- Bruk `Promise.all()` for kritiske, alt-eller-ingenting dataavhengigheter.
- Stol på `Promise.allSettled()` for å bygge robuste brukergrensesnitt med uavhengige komponenter.
- Anvend `Promise.race()` for å håndheve tidsbegrensninger og forhindre uendelig venting.
- Velg `Promise.any()` for å skape raske og feiltolerante systemer med redundante datakilder.
Neste gang du skriver flere `await`-setninger på rad, ta en pause og spør: "Er disse operasjonene virkelig avhengige av hverandre?" Hvis svaret er nei, har du en gyllen mulighet til å refaktorere koden din for samtidighet. Begynn å initiere promisene dine sammen, velg riktig kombinator for logikken din, og se applikasjonens ytelse skyte i været.